<?php
/**
 * Library Of Shared Code (LOSC)
 *
 * LICENSE
 *
 * This source file is subject to the New BSD License that is bundled
 * with this package in the file LICENSE.txt.
 *
 * @category   LOSC Framework
 * @package    Security
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */

/**
 * Access Control List implementation which loads from database using Doctrine
 *
 * @category   LOSC Framework
 * @package    Security
 * @copyright  Copyright (c) 2008 Robin Skoglund (http://robinsk.net/)
 * @license    http://creativecommons.org/licenses/BSD/  New BSD License
 */
class Losc_Security_Acl_Doctrine extends Zend_Acl
{
    /**
     * Singleton: ACL instance
     *
     * @var Losc_Security_Acl_Doctrine
     */
    private static $_aclInstance;
    
    /**
     * Currently active user
     *
     * @var App_User
     */
    private $_user = null;
    
    /**
     * Contains all of the user's active role IDs
     *
     * @var array
     */
    private $_activeRoles = array();
    
    /**
     * Whether dispatching non-existent should allow/deny access by default
     *
     * @var bool
     */
    private $_defaultAllowDispatch;
    
    /**
     * Singleton: Only self may construct
     *
     */
    private function __construct()
    {
        // get user
        $this->_user = App_User::getCurrentUser();
        
        // fetch roles and determine active roles
        $this->_initRoles();
        
        // fetch mvc resources
        $this->_initResources();
        
        // fetch mvc rules
        $this->_initRules();
        
        // get option(s) from db
        $this->_defaultAllowDispatch =
            Losc_App_Options::getOption('defaultAllowDispatch', 'auth');
    }
    
    /**
     * Singleton: Cloning disabled
     *
     */
    private function __clone()
    {
        // cloning disabled
    }
    
    /**
     * Returns the static instance of the ACL
     *
     * @return Losc_Security_Acl_Doctrine
     */
    public static function getInstance()
    {
        if (self::$_aclInstance === null) {
            self::$_aclInstance = new self();
        }
        
        return self::$_aclInstance;
    }
    
    /**
     * Fetches roles and determines active roles based on current user
     *
     * @return void
     */
    private function _initRoles()
    {
        $q = Doctrine_Query::create()
             ->select('r.role, r.parent')
             ->from('Security_Role r');
        
        $result = $q->execute(array(), Doctrine::HYDRATE_ARRAY);
        
        $round     = 0;
        $maxRounds = count($result);
        while (count($result)) {
            foreach ($result as $key => $r) {
                if (!$r['parent']) {
                    $this->addRole(new Zend_Acl_Role($r['role']));
                    unset($result[$key]);
                } elseif ($this->hasRole($r['parent'])) {
                    $this->addRole(new Zend_Acl_Role($r['role']), $r['parent']);
                    unset($result[$key]);
                }
            }
            
            if (++$round >= $maxRounds) {
                $msg = 'Corruption in role table. Unsortable dependencies';
                throw new Losc_Security_Exception($msg);
            }
        }
        
        // add current user's roles as active roles
        foreach ($this->_user->roles as $role) {
            $this->_activeRoles[] = $role->getRoleId();
        }
        
        // add user as role (which inherits the user's roles)
        $this->addRole($this->_user, $this->_activeRoles);
    }
    
    /**
     * Fetches resources from db
     *
     * @return void
     */
    private function _initResources()
    {
        // TODO: implement
    }
    
    /**
     * Fetches rules from db
     *
     * @return void
     */
    private function _initRules()
    {
        $q = Doctrine_Query::create()
             ->from('Security_MvcRule r1')
             ->leftJoin('r1.resource r2')
             ->select('r2.module, r2.controller, r1.privilege, r1.role_id');
        
        $r = $q->execute(array(), Doctrine::HYDRATE_ARRAY);
        
        foreach ($r as $rule) {
            $resource = 'mvc:' . $rule['resource']['module'];
            $parent = null;
            if ($rule['resource']['controller']) {
                $parent = $resource;
                if (!$this->has($parent)) {
                    $parent = new Zend_Acl_Resource($parent);
                    $this->add($parent);
                }
                
                $resource .= '.' . $rule['resource']['controller'];
            }
            
            if (!$this->has($resource)) {
                $resource = new Zend_Acl_Resource($resource);
                $this->add($resource, $parent);
            }
            
            if ($rule['privilege']) {
                $this->allow($rule['role_id'], $resource, $rule['privilege']);
            } else {
                $this->allow($rule['role_id'], $resource);
            }
        }
    }
    
    /**
     * Checks if the current user is allowed to dispatch the given action
     *
     * @param string $module      module name
     * @param string $controller  controller name
     * @param string $action      action name
     */
    public function isAllowedAction($module, $controller, $action)
    {
        $moduleResource = 'mvc:' . $module;
        $fullResource = $moduleResource . '.' . $controller;
        
        if ($this->has($fullResource)) {
            // full resource name exists
            return $this->isAllowed($this->_user, $fullResource, $action);
        } elseif ($this->has($moduleResource)) {
            // module resource exists
            return $this->isAllowed($this->_user, $moduleResource, $action);
        } else {
            // there is no resource for module/controller, return default
            return (bool) $this->_defaultAllowDispatch;
        }
    }
    
    /**
     * Checks if the current user is allowed to dispatch the given request
     *
     * @param Zend_Controller_Request_Http $request  request object
     */
    public function isAllowedRequest(Zend_Controller_Request_Http $request)
    {
        return $this->isAllowedAction($request->getModuleName(),
                                      $request->getControllerName(),
                                      $request->getActionName());
    }
}
